classdef dotsAllRemoteObjectManagers < dotsAllObjectManagers & topsConcurrent
    % @class dotsAllRemoteObjectManagers
    % Superclass for singleton objects that coordinate remote machine
    % behaviors.
    % @details
    % dotsAllRemoteObjectManagers extends the dotsAllObjectManagers
    % superclass to coordinate between serparate instances of Matlab
    % and separate machines.  Each remote manager object can communicate
    % with its remote counterpart, which is a manager of the same class
    % running in a different Matab instance.  The counterpart can in turn
    % coordinate behaviors on behalf of its managed objects.
    % @details
    % Behaviors to coordinate remotely might include creation and deletion
    % of objects or specific object behaviors like drawing graphics or
    % playing sounds.
    % @details
    % dotsTheMessenger handles communications between remote manager
    % counterparts.  Comminications settings, like IP addresses and ports,
    % should be set up using dotsTheMachineConfiguration.
    % @details
    % Remote managers use the clientMode and serverMode shared properties
    % to guide their behaviors (see dotsTheSwitchboard for more shared
    % properties).  When clientMode is true, a manager will try to delegate
    % behaviors to its remote counterpart.  When serverMode is true, a
    % manager will try to carry out behaviors that were delegated to it.
    % @details
    % Depending on shared property values and communications settings,
    % remote managers may operate in four distinct ways:
    %   - <b>No Delegation</b>: if clientMode and serverMode are both
    %   false, remote managers will carry out behaviors themselves
    %   without sending messages.
    %   - <b>Self Delegation</b>: if clientMode and serverMode are both
    %   true, and communications settings all refer to the local machine,
    %   remote  managers will delegate behaviors to themselves.  They will
    %   send messages through dotsTheMessenger, but the surface behavior is
    %   expected to be the same as with "No Delegation".
    %   - <b>Client Delegation</b>: if clientMode is true, remote
    %   managers will delegate behaviors by sending messages through
    %   dotsTheMessengter to whatever machine is specified in the
    %   communictations settings.
    %   - <b>Server Compliance</b>: if serverMode is true, remote
    %   managers will carry out behaviors as requested in messages sent
    %   from a "clientMode" counterpart.
    %   .
    % "No Delegation" should produce the fastest behavior within a single
    % instance of Matlab.  "Self Delegation" is useful for testing
    % delegation and messaging within a single instance of Matlab.
    % "Client Delegation" and "Server Compliance" are complementary.  They
    % allow Snow Dots to span more than one instance of Matlab and more
    % than one machine.
    % @details
    % dotsAllRemoteObjectManagers itself is not a usable class, it just
    % provides functionality to subclasses (see below).
    % @ingroup foundataion
    
    properties
        % true/false, whether to delegate behaviors to the server
        % counterpart
        % @details
        % Automatically opts in to share via dotsTheSwitchboard.
        clientMode;
        
        % string IP address to use for client behaviors
        % @details
        % Automatically gets a machine-specific address from
        % dotsTheMachineConfiguration.
        clientIP;
        
        % integer port number to use for client behaviors
        % @details
        % Automatically gets a machine-specific port number from
        % dotsTheMachineConfiguration.
        clientPort;
        
        % socked identifier for doing client behaviors
        % @details
        % Automatically opens a socket with dotsTheMessenger.
        clientSocket;
        
        % true/false, whether to carry out behaviors for the client
        % counterpart
        % @details
        % Automatically opts in to share via dotsTheSwitchboard.
        serverMode;
        
        % string IP address to use for server behaviors
        % @details
        % Automatically gets a machine-specific address from
        % dotsTheMachineConfiguration.
        serverIP;
        
        % integer port number to use for server behaviors
        % @details
        % Automatically gets a machine-specific port number from
        % dotsTheMachineConfiguration.
        serverPort;
        
        % socked identifier for doing server behaviors
        % @details
        % Automatically opens a socket with dotsTheMessenger.
        serverSocket;
        
        % containers.Map of structs describing delegated behaviors
        % @details
        % Keeps track of all behaviors that a client manager delegates to
        % its server counterpart, including still-open and already-closed
        % behaviors.
        transactions;
        
        % count of delegated behaviors that were already carried out
        transactionsClosed;
        
        % count of delegated behaviors that were never acknowledged
        transactionsFailed;
        
        % time to wait while the server counterpart carries out
        % transactions
        % @details
        % transactionsTimeout should be in units of clockFunction.
        % @details
        % A client manager waits up to this amount of time to allow its
        % server counterpart to carry out a transaction.  Then it complains
        % and waits again.
        transactionsTimeout;
        
        % fevalable cell array which yields a short time time to the OS or
        % Matlab
        % @details
        % Automatically opts in to share via dotsTheSwitchboard.
        waitFevalable;
    end
    
    properties (Access = protected)
        % reusable struct to represent transactions with the counterpart
        % @details
        % transactionTemplate has the same fields as a dotsTransaction
        % object.  For performance reasons, its stored as a struct instead
        % of an object.
        transactionTemplate;
        
        % the dotsTheMessenger singleton
        theMessenger;
        
        % true or false whether a new transaction is allowed right now
        % @details
        % The server counterpart sets mayRequestTransaction to false while
        % it's busy carrying out a transaction.
        mayRequestTransaction;
        
        % true or false whether a transaction is in progress right now
        % @details
        % The client counterpart sets isDoingTransaction to true while
        % it's waiting for the server counterpart to carry out a
        % transaction and sets isDoingTransaction when closing a
        % transaction.
        isDoingTransaction;
    end
    
    properties (Hidden)
        % placeholder value used during transaction benchmarking
        echo;
    end
    
    methods
        % Restore the manager singleton to a fresh state.
        function initialize(self)
            self.mayRequestTransaction = false;
            
            self.initialize@dotsAllObjectManagers;
            self.transactions = containers.Map('a', struct, ...
                'uniformValues', false);
            self.transactions.remove(self.transactions.keys);
            
            self.isDoingTransaction = false;
            self.transactionsClosed = 0;
            self.transactionsFailed = 0;
            self.transactionTemplate = ...
                dotsTransaction.newTransactionStruct;
            
            self.theMessenger = dotsTheMessenger.theObject;
            self.configureSockets;
            
            if self.clientMode && ~self.serverMode
                self.requestTransaction([], [], false, ...
                    'softReset', {});
            end
            
            dotsTheSwitchboard.sharePropertyOfObject('waitFevalable', self);
            
            self.mayRequestTransaction = true;
        end
        
        % Remove managed objects and flush sockets.
        function softReset(self)
            self.managedObjects.removeAllGroups;
            self.theMessenger.flushSocket(self.serverSocket);
            self.theMessenger.flushSocket(self.clientSocket);
        end
        
        % Carry out one transaction, if one arrived at serverSocket.
        % @details
        % If serverMode is true, fulfils one service transaction at a time
        % and returns.
        function runBriefly(self)
            if self.serverMode
                [txn, status] = ...
                    self.theMessenger.receiveMessageAtSocket( ...
                    self.serverSocket);
                if status >= 0
                    self.carryOutTransaction(txn);
                end
            end
        end
        
        % Continue running for the given duration.
        % @param duration time in units of clockFunction to continually
        % invoke runBriefly().
        % @details
        % Executes run-like behavior for a limited time, by repeatedly
        % invoking runBriefly().
        function runForDuration(self, duration)
            endTime = feval(self.clockFunction) + duration;
            self.start;
            while feval(self.clockFunction) < endTime
                self.runBriefly;
            end
            self.finish;
        end
        
        % Get information about delegated, unfinished behaviors.
        % @details
        % Returns a struct array of dotsTransaction structs representing
        % behaviors that the client manager has delegated to the server
        % counterpart, but the server counterpart failed to acknowledge or
        % carry out.
        function txns = getFailedTransactions(self)
            allTxns = self.transactions.values;
            isFailed = false(1, length(allTxns));
            for ii = 1:length(allTxns)
                txn = allTxns{ii};
                isFailed(ii) = isempty(txn.acknowledgeTime) ...
                    || isempty(txn.finishTime);
            end
            txns = [allTxns{isFailed}];
        end
        
        % Set a manager property without reporting it to the counterpart.
        % @param property string name of one of the manager's properties
        % @param value the value to assign to @a property
        % @details
        % setPropertySilently() is like the setPropertySilently() method of
        % the dotsAllManagedObjects superclass.  It allows the manager to
        % treat itself like a managed object in some cases.
        function setPropertySilently(self, property, value)
            self.(property) = value;
        end
        
        % Managed objects must report property changes here.
        % @param object one of the dotsAllManagedObjects objects listed in
        % managedObjects
        % @param property the string name of the property of @a object that
        % recently changed.
        % @details
        % Extends the same method of dotsAllObjectManagers to also
        % coordinate with the remote counterpart.
        function acknowledgePropertyChange(self, object, property)
            if self.clientMode && self.mayRequestTransaction
                self.requestTransaction(object, property, false, ...
                    'setObjectProperty', {property, object.(property)});
            end
        end
        
        % Managed objects should be created with this method.
        % @param className the string name of a dotsAllManagedObjects
        % subclass
        % @param size optional size of object array to create
        % @details
        % Extends the same method of dotsAllObjectManagers to also
        % coordinate with the remote counterpart.
        % @details
        % Requests that the server counterpart creates new objects as well,
        % but only the server counterpart is not also this manager.
        function object = newObject(self, className, size)
            if nargin < 3
                size = [1,1];
            end
            object = self.newObject@dotsAllObjectManagers(className, size);
            
            if self.clientMode && ~self.serverMode
                UID = {object.UID};
                self.requestTransaction([], [], false, ...
                    'newObjectWithUID', {className, size, UID});
            end
        end
        
        % Stop managing an object.
        % @param object one of the dotsAllManagedObjects objects listed in
        % managedObjects
        % @details
        % Extends the same method of dotsAllObjectManagers to also
        % coordinate with the remote counterpart.
        function removeObject(self, object, varargin)
            self.removeObject@dotsAllObjectManagers( ...
                object, varargin{:});
            
            if self.clientMode && ~self.serverMode
                self.requestTransaction( ...
                    object, [], false, 'removeObject', varargin);
            end
        end
        
        % Add an object to a custom group.
        % @param object one of the dotsAllManagedObjects objects listed in
        % managedObjects
        % @param group string name for a group that @a object should join
        % @param mnemonic list mnemonic to use instead of @a object.UID
        % @details
        % @details
        % Extends the same method of dotsAllObjectManagers to also
        % coordinate with the remote counterpart.
        function addObjectToGroup(self, object, varargin)
            self.addObjectToGroup@dotsAllObjectManagers( ...
                object, varargin{:});
            
            if self.clientMode && self.mayRequestTransaction
                self.requestTransaction( ...
                    object, [], false, 'addObjectToGroup', varargin);
            end
        end
        
        % Remove an object from a custom group.
        % @param object one of the dotsAllManagedObjects objects listed in
        % managedObjects
        % @param group string name for a group that @a object should quit.
        % @details
        % Extends the same method of dotsAllObjectManagers to also
        % coordinate with the remote counterpart.
        function removeObjectFromGroup(self, object, varargin)
            self.removeObjectFromGroup@dotsAllObjectManagers( ...
                object, varargin{:});
            
            if self.clientMode && ~self.serverMode
                self.requestTransaction( ...
                    object, [], false, 'removeObjectFromGroup', varargin);
            end
        end
        
        % Set one property for one object.
        % @param object one of the dotsAllManagedObjects objects listed in
        % managedObjects
        % @param property the string name of a property of @a object
        % @param value the value to assign to @a property
        % @details
        % Extends the same method of dotsAllObjectManagers to also
        % coordinate with the remote counterpart.
        function setObjectProperty(self, object, property, value)
            self.setObjectProperty@dotsAllObjectManagers( ...
                object, property, value);
            
            if self.clientMode && self.mayRequestTransaction
                args = {property, value};
                self.requestTransaction( ...
                    object, property, false, 'setObjectProperty', args);
            end
        end
        
        % Set multiple properties for one object.
        % @param object one of the dotsAllManagedObjects objects listed in
        % managedObjects
        % @param varargin pairs of string property names and values to set
        % to @a object.
        % @details
        % Extends the same method of dotsAllObjectManagers to also
        % coordinate with the remote counterpart.
        function setObjectPropertiesMany(self, object, varargin)
            self.setObjectPropertiesMany@dotsAllObjectManagers( ...
                object, varargin{:});
            
            if self.clientMode && self.mayRequestTransaction
                self.requestTransaction( ...
                    object, [], false, 'setObjectPropertiesMany', varargin);
            end
        end
        
        % Get one property value from one object.
        % @param object one of the dotsAllManagedObjects objects listed in
        % managedObjects
        % @param property the string name of a property of @a object
        % @details
        % Extends the same method of dotsAllObjectManagers to also
        % coordinate with the remote counterpart.
        function value = getObjectProperty(self, object, property)
            if self.clientMode && self.mayRequestTransaction
                self.requestTransaction( ...
                    object, property, true, 'getObjectProperty', {property});
            end
            
            value = self.getObjectProperty@dotsAllObjectManagers( ...
                object, property);
        end
        
        % Set one property for multiple objects.
        % @param group one of the group names in managedObjects
        % @param property the string name of a property shared by all the
        % objects in @a group.
        % @param value the value to assign to @a property
        % @details
        % Extends the same method of dotsAllObjectManagers to also
        % coordinate with the remote counterpart.
        function setGroupProperty(self, varargin)
            self.setGroupProperty@dotsAllObjectManagers(varargin{:});
            
            if self.clientMode && self.mayRequestTransaction
                self.requestTransaction( ...
                    [], [], false, 'setGroupProperty', varargin);
            end
        end
        
        % Call a method on one object.
        % @param object one of the dotsAllManagedObjects objects listed in
        % managedObjects
        % @param method the string name of a method of @a object
        % @param varargin optional arguments to pass to @a method.
        % @details
        % Extends the same method of dotsAllObjectManagers to also
        % coordinate with the remote counterpart.
        function callObjectMethod(self, object, varargin)
            if self.clientMode && self.mayRequestTransaction
                self.requestTransaction( ...
                    object, [], false, 'callObjectMethod', varargin);
            else
                self.callObjectMethod@dotsAllObjectManagers( ...
                    object, varargin{:});
            end
        end
        
        % Call a method on one object and assign the return value.
        % @param object one of the dotsAllManagedObjects objects listed in
        % managedObjects
        % @param method the string name of a method of @a object
        % @param returnProperty the string name of a property of @a object
        % which will receive the return value from @a method
        % @param varargin optional arguments to pass to @a method
        % @details
        % Extends the same method of dotsAllObjectManagers to also
        % coordinate with the remote counterpart.
        function value = getObjectMethodResult(self, ...
                object, method, returnProperty, varargin)
            if self.clientMode && self.mayRequestTransaction
                args = cat(2, {method, returnProperty}, varargin);
                self.requestTransaction( ...
                    object, returnProperty, true, ...
                    'getObjectMethodResult', args);
                value = object.(returnProperty);
            else
                value = self.getObjectMethodResult@dotsAllObjectManagers( ...
                    object, method, returnProperty, varargin{:});
            end
        end
        
        % Call a method on multiple objects.
        % @param group one of the group names in managedObjects
        % @param method the string name of a method shared by all the
        % objects in @a group.
        % @param varargin optional arguments to pass to @a method.
        % @details
        % Extends the same method of dotsAllObjectManagers to also
        % coordinate with the remote counterpart.
        function callGroupMethod(self, varargin)
            if self.clientMode && self.mayRequestTransaction
                self.requestTransaction( ...
                    [], [], false, 'callGroupMethod', varargin);
            else
                self.callGroupMethod@dotsAllObjectManagers(varargin{:});
            end
        end
        
        % Update object properties with values frmo the server.
        % @param object one of the dotsAllManagedObjects objects listed in
        % managedObjects
        % @details
        % The client manager requests the value of each SetObservable
        % property of @a object from the server counterpart and assigns the
        % values to the local @a object.  This is necessary when property
        % changes or method invocations cause the @a object on the server
        % to update its properties.  Since all transactions must originate
        % on the client side, these property updates cannot be reported to
        % the client automatically.
        % @details
        % updateClientObjectProperties() is provided for convenience.  When
        % you know the name of the property or properties to update,
        % getObjectProperty() will be faster.
        function updateClientObjectProperties(self, object)
            metaClass = metaclass(object);
            if isprop(metaClass, 'PropertyList')
                metaProps = metaClass.PropertyList;
            else
                metaProps = [metaClass.Properties{:}];
            end
            isSetObservable = [metaProps.SetObservable];
            properties = {metaProps(isSetObservable).Name};
            for ii = 1:numel(properties)
                self.getObjectProperty(object, properties{ii});
            end
        end
        
        
        % Measure client-server message round trip times.
        % @param doPlot true/false, whether to open a figure and plot
        % results, default is false.
        % @param iterations, number of transactions to test, default is
        % 100.
        % @details
        % The client manager sends @a iterations values to the server
        % counterpart, to be sent back immediately.  Returns a struct array
        % containing timing and correctness information from each round
        % trip.  If doPlot is also opens a figure and plots the
        % information.
        function results = benchmarkTransactions(self, doPlot, iterations)
            if nargin < 2
                doPlot = false;
            end
            
            if nargin < 3
                iterations = 100;
            end
            
            results.roundTripTime = zeros(1, iterations);
            results.timeToAcknowledge = zeros(1, iterations);
            results.timeToDoAction = zeros(1, iterations);
            results.valueIsCorrect = false(1, iterations);
            
            % repeat the first test, which may be bothered by Matlab's
            % function loading
            for ii = [1 1:iterations]
                % send a value for the server counterpart to echo
                self.echo = nan;
                sentValue = ii;
                txn = self.requestTransaction( ...
                    [], 'echo', true, 'echoValue', {sentValue});
                echoedValue = self.echo;
                
                % get the updated complete transaction
                txn = self.transactions(txn.UID);
                if ~isempty(txn.finishTime)
                    results.roundTripTime(ii) = ...
                        txn.finishTime - txn.startTime;
                    results.timeToDoAction(ii) = ...
                        txn.actionFinishTime - txn.actionStartTime;
                    results.timeToAcknowledge(ii) = ...
                        txn.acknowledgeTime;
                    results.valueIsCorrect(ii) = ...
                        sentValue == echoedValue;
                end
            end
            
            if doPlot
                f = figure;
                clf(f)
                set(f, 'NumberTitle', 'off', ...
                    'Name', sprintf('%s: clientMode=%d serverMode=%d', ...
                    class(self), self.clientMode, self.serverMode))
                ax = subplot(4,1,1:3, 'Parent', f);
                xaxis = 1:iterations;
                isCorrect = results.valueIsCorrect;
                names = {'roundTripTime', ...
                    'timeToAcknowledge', ...
                    'timeToDoAction'};
                yTicks = [];
                lines = [];
                for ii = 1:length(names);
                    times = results.(names{ii});
                    color = dec2bin(ii,3)=='1';
                    lines(end+1) = ...
                        line(xaxis(isCorrect), times(isCorrect), ...
                        'Parent', ax, ...
                        'DisplayName', names{ii}, ...
                        'Color', color, ...
                        'Linestyle', 'none', ...
                        'Marker', '.');
                    if any(~isCorrect)
                        lines(end+1) = ...
                            line(xaxis(~isCorrect), times(~isCorrect), ...
                            'Parent', ax, ...
                            'DisplayName', '(corrupt value)', ...
                            'Color', color, ...
                            'Linestyle', 'none', ...
                            'Marker', 'x');
                    end
                    yTicks(end+1) = mean(times);
                    yTicks(end+1) = max(times);
                end
                title(ax, 'How long did parts of each transaciton take?');
                xlabel(ax, 'iteration');
                ylabel(ax, sprintf('time from %s', ...
                    func2str(self.clockFunction)));
                set(ax, 'XLim', [0, 1+iterations]);
                if any(yTicks>0)
                    set(ax, ...
                        'YLim', [0, 1.05*max(yTicks)], ...
                        'YTick', unique(sort(yTicks)));
                end
                legendNames = get(lines, 'DisplayName');
                legend(ax, legendNames{:}, 'Location', 'southeast')
                
                % compare client and server clocks
                serverToClient = self.estimateServerClockDriftAndOffset;
                Rinv = inv(serverToClient.polyInfo.R);
                normr = serverToClient.polyInfo.normr;
                df = serverToClient.polyInfo.df;
                covEstimate = (Rinv*Rinv')*normr^2/df;
                driftStdEst = sqrt(covEstimate(1,1));
                offsetStdEst = sqrt(covEstimate(2,2));
                
                infoString = sprintf('');
                rows = {'drift rate', 'offset'};
                cols = {'estimate', 'std'};
                data = { ...
                    serverToClient.drift, driftStdEst; ...
                    serverToClient.offset, offsetStdEst};
                clockTable = uitable( ...
                    'Parent', f, ...
                    'Units', 'normalized', ...
                    'Position', [.1 .05, .8 .125], ...
                    'Data', data, ...
                    'ColumnName', cols, ...
                    'RowName',  rows);
                clockLabel = uicontrol( ...
                    'Parent', f, ...
                    'Style', 'text', ...
                    'Units', 'normalized', ...
                    'Position', [.1 .175, .8 .05], ...
                    'HorizontalAlignment', 'center', ...
                    'String', 'How is the server clock different from the client clock?');
            end
        end
        
        % Use previous transactions to characterize the server clock.
        % @param iterations optional number of transacitons to carry out,
        % to generate transaction data.  Default is 0.
        % @param cutoffTime optional time limit, in units of clockFunctionm
        % used to ignore transactions that take too long.  Default is inf.
        % @details
        % Uses data stored in transactions to estimate the drift and offset
        % of the server counterpart's clock, relative to the client's
        % own clockFunction.  This is only possible when clientMode is
        % true since otherwise the manager will not use transactions.
        % Furthermore, this is only useful when serverMode is false, since
        % that is when the manager will do transactions with a server
        % counterpart that's running in a separte Matlab instance or
        % machine.
        % @details
        % If @a iterations is finite and nonzero, will do the given number
        % of transactions with the server counterpart in order to generate
        % extra transaction timing data.  Otherwise, only uses previously
        % generated data stored in transactions.
        % @details
        % If @a cutoffTime is provided, will ignore transactions that took
        % longer than the given duration to be acknowledged by the server
        % counterpart.  This allows potentially unreliable timing data to
        % be ignored.  The default is to use all of the "acknowledge" times
        % from the dotsTransaction structs in transactions.
        % @details
        % Always ignores data from the earliest transaction, since it may
        % be disturbed by Matlab's function loading time.
        % @details
        % Returns the struct generated from estimateClockDriftAndOffset()
        % suitable for converting times recorded by the server counterpart
        % into the frame of reference of the client's own clockFuncion.  If
        % transactions contains no data, returns [].
        % @details
        % User applyClockDriftAndOffset(), along with the returned struct,
        % to convert server timing data into the client's frame of
        % reference.
        function serverToClient = estimateServerClockDriftAndOffset( ...
                self, iterations, cutoffTime)
            
            serverToClient = [];
            
            if nargin > 1 && isfinite(iterations) && iterations > 0
                self.benchmarkTransactions(false, iterations);
            end
            
            if nargin < 3
                cutoffTime = inf;
            end
            
            if self.transactions.length > 1
                transactionVals = self.transactions.values;
                txn = transactionVals{1};
                fn = fieldnames(txn);
                for ii = 1:numel(transactionVals)
                    txn = transactionVals{ii};
                    crap = setdiff(fn, fieldnames(txn));
                    crap{:}
                end
                
                txns = [transactionVals{:}];
                
                clientTimes = [txns.startTime];
                serverTimes = [txns.actionStartTime];
                ackTimes = [txns.acknowledgeTime];
                isOK = ackTimes <= cutoffTime;
                [m, first] = min(clientTimes);
                isOK(first) = false;
                
                serverToClient = estimateClockDriftAndOffset( ...
                    serverTimes(isOK) - ackTimes(isOK)/2, ...
                    clientTimes(isOK));
            end
        end
    end
    
    methods (Access = protected)
        % Open socket for client or server behavior, or none, or both.
        function configureSockets(self)
            self.theMessenger.closeSocket(self.clientSocket);
            self.clientSocket = -1;
            
            self.theMessenger.closeSocket(self.serverSocket);
            self.serverSocket = -1;
            
            if self.clientMode
                self.clientSocket = self.theMessenger.openSocket( ...
                    self.clientIP, self.clientPort, ...
                    self.serverIP, self.serverPort);
            end
            
            if self.serverMode
                self.serverSocket = self.theMessenger.openSocket( ...
                    self.serverIP, self.serverPort, ...
                    self.clientIP, self.clientPort);
            end
        end
        
        % Delegate a behavior to the server counterpart.
        % @param object managed object to pass to @a methodName--may be
        % empty
        % @param propertyName managed object property to get, set, or
        % receive a return value--may be empty
        % @param methodName a dotsAllObjectManagers public method
        % @param hasOutput true or false whether to expect an output
        % from @a methodName
        % @param methodArgs additional arguments to pass to @a methodName
        % @details
        % Returns a dotsTransaction struct that represents the desired
        % behavior.  The transaction struct will contain the UIDs of
        % objects to be resolved and passed to the counterpart's @a
        % methodName.
        % @details
        % Sends the transaction through dotsTheMessenger using
        % clientSocket, to be carried out by the server counterpart (even
        % if the manager is acting as both client and server).
        % @details
        % Blocks until the transaction has been carried out and
        % acknowledged.
        % @details
        % Sets the startTime of the transaction, using the client
        % managers's clockFunction.
        function txn = requestTransaction(self, object, propertyName, ...
                hasOutput, methodName, methodArgs)
            
            txn = self.transactionTemplate;
            if ~isempty(object)
                txn.objectUID = object.UID;
            end
            txn.propertyName = propertyName;
            txn.hasOutput = hasOutput;
            txn.methodName = methodName;
            txn.methodArgs = methodArgs;
            
            txn.UID = self.getUIDForClassName(txn.methodName);
            self.transactions(txn.UID) = txn;
            
            % server counterpart must acknowledge
            %   when messaging between Matlab instances
            sock = self.clientSocket;
            txn.startTime = feval(self.clockFunction);
            mustAcknowledge = xor(self.clientMode, self.serverMode);
            [status, ackTime] = self.theMessenger.sendMessageFromSocket( ...
                txn, sock, mustAcknowledge);
            
            if status == self.theMessenger.notAcknowledgedStatus
                % server is not responding
                disp(sprintf('%s: server did not respond(%d)', ...
                    class(self), status));
                self.transactionsFailed = self.transactionsFailed + 1;
                
            else
                % wait for server to return transaction data
                self.isDoingTransaction = true;
                timeout = self.transactionsTimeout;
                waitFevalable = {@runBriefly, self};
                while self.isDoingTransaction
                    [txn, status] = ...
                        self.theMessenger.waitForMessageAtSocket( ...
                        sock, timeout, waitFevalable);
                    if status == self.theMessenger.timedOutStatus
                        disp(sprintf('%s: waiting for server behavior(%d)', ...
                            class(self), status));
                    elseif status >= 0
                        txn.acknowledgeTime = ackTime;
                        txn = self.closeTransaction(txn);
                    end
                end
            end
        end
        
        % Carry out a behavior delegated by the client counterpart.
        % @param txn a dotsTransaction struct that represents the desired
        % behavior.
        % @details
        % Resolves objects locally that correspond to the UIDs in @a
        % txn.objectUID, and invokes the specified dotsAllObjectManagers
        % public method, with the specified arguments.  If @a
        % txn.hasOutput is true, fills in @a txn.returnValue with
        % the value returned from the method invokation.
        % @details
        % Sends the transaction back, through dotsTheMessenger using
        % serverSocket, to be acknowledgedout by the client counterpart
        % (even if the manager is acting as both client and server).
        % @details
        % Sets the actionStartTime and actionFinishTime of the @a txn,
        % using the server managers's clockFunction.  Returns the modified
        % @a txn.
        function txn = carryOutTransaction(self, txn)
            self.mayRequestTransaction = false;
            
            txn.actionStartTime = feval(self.clockFunction);
            
            if isempty(txn.objectUID)
                args = txn.methodArgs;
            else
                object = self.getObjectByUID(txn.objectUID);
                args = cat(2, {object}, txn.methodArgs);
            end
            
            if txn.hasOutput
                txn.returnValue = self.(txn.methodName)(args{:});
            else
                self.(txn.methodName)(args{:});
            end
            
            txn.actionFinishTime = feval(self.clockFunction);
            
            % client counterpart must acknowledge
            %   when messaging across Matlab instances
            mustAcknowledge = xor(self.clientMode, self.serverMode);
            [status, ackTime] = self.theMessenger.sendMessageFromSocket( ...
                txn, self.serverSocket, mustAcknowledge);
            
            self.mayRequestTransaction = true;
        end
        
        % Acknowledge that a delegated behavior was carried out.
        % @param txn a dotsTransaction struct representing a previously
        % delegated behavior.requestTransaction
        % @details
        % If @a txn.hasOutput is true, fills in the @a txn.propertyName
        % property of the object given by @a txn.objectUID with the value
        % returned from the method invokation, @a txn.returnValue.
        % @details
        % Sets the finishTime of the transaction, using the client
        % managers's clockFunction.  Returns the modified @a txn.
        function txn = closeTransaction(self, txn)
            if txn.hasOutput
                object = self.getObjectByUID(txn.objectUID);
                object.setPropertySilently(...
                    txn.propertyName, txn.returnValue);
            end
            
            % replace the listed transaction with the closed one
            txn.finishTime = feval(self.clockFunction);
            self.transactions(txn.UID) = txn;
            
            self.isDoingTransaction = false;
            self.transactionsClosed = self.transactionsClosed + 1;
        end
        
        % Return the given value (used for transaction benchmarking).
        function value = echoValue(self, value)
        end
    end
end